﻿using System;
using System.Collections;

namespace HART.Messages
{
    /// <summary>
    /// 实现从主设备发送到从设备的请求消息。
    /// </summary>
    public class Request : MessageBase
    {
        /// <summary>
        /// 如果请求由次主设备发送，则为 <see langword="true"/>。
        /// </summary>
        public bool IsSecondaryMaster { get; }

        /// <summary>
        /// 如果请求应通过广播地址发送，则为 <see langword="true"/>。
        /// </summary>
        public bool IsBroadcastAddress { get; set; }

        /// <summary>
        /// 创建一个用于广播地址的请求。
        /// </summary>
        /// <param name="isSecondaryMaster">如果请求由次主设备发送，则为 <see langword="true"/>。</param>
        public Request(
            bool isSecondaryMaster
            ) : base(SetAddress(isSecondaryMaster))
        {
            IsSecondaryMaster = isSecondaryMaster;
        }

        /// <summary>
        /// 创建一个用于短帧格式地址的请求。
        /// <para>允许的地址范围为 0..15。</para>
        /// </summary>
        /// <param name="isSecondaryMaster">如果请求由次主设备发送，则为 <see langword="true"/>。</param>
        /// <param name="deviceAddress">设备地址。</param>
        public Request(
            bool isSecondaryMaster,
            int deviceAddress
            ) : base(SetAddress(isSecondaryMaster, deviceAddress))
        {
            IsSecondaryMaster = isSecondaryMaster;
            FrameFormat = FrameFormats.Short;
        }

        /// <summary>
        /// 创建一个用于长帧格式地址的请求。
        /// </summary>
        /// <param name="isSecondaryMaster">如果请求由次主设备发送，则为 <see langword="true"/>。</param>
        /// <param name="manufacturerId">HART 制造商 ID。</param>
        /// <param name="deviceTypeCode">设备类型代码。</param>
        /// <param name="deviceSerialNumber">设备序列号。</param>
        public Request(
            bool isSecondaryMaster,
            int manufacturerId,
            int deviceTypeCode,
            int deviceSerialNumber
            ) : base(SetAddress(isSecondaryMaster, manufacturerId, deviceTypeCode, deviceSerialNumber))
        {
            IsSecondaryMaster = isSecondaryMaster;
            FrameFormat = FrameFormats.Long;
        }

        /// <summary>
        /// 序列化请求。
        /// </summary>
        /// <returns>准备通过 HART 协议发送的字节数组。</returns>
        public byte[] Serialize()
        {
            var index = 0;
            var count = 1;

            var preamble = GetPreamble();
            count += preamble.Length;

            var limiter = GetLimiter();
            count += limiter.Length;

            var address = Address;
            count += address.Length;

            var command = GetCommand();
            count += command.Length;

            var data = Data;
            if (data != null) count += data.Length;

            var byteCounter = GetByteCounter(data);
            count += byteCounter.Length;

            var result = new byte[count];

            preamble.CopyTo(result, index);
            limiter.CopyTo(result, index += preamble.Length);
            address.CopyTo(result, index += limiter.Length);
            command.CopyTo(result, index += address.Length);
            data?.CopyTo(result, index += command.Length);
            byteCounter.CopyTo(result, index += data?.Length ?? command.Length);

            var checkSum = GetCheckSum(result);
            checkSum.CopyTo(result, index + byteCounter.Length);

            return result;
        }

        /// <summary>
        /// 生成消息的前导码 (<see cref="MessageBase.Preamble"/>)。
        /// </summary>
        /// <returns>前导码的字节数组格式。</returns>
        private byte[] GetPreamble()
        {
            if (Preamble <= 0)
                throw new ArgumentException("前导码的字符数必须大于零。");

            var res = new byte[Preamble];

            for (var i = 0; i < Preamble; i++)
                res[i] = PreambleSymbol;

            return res;
        }

        /// <summary>
        /// 填充消息的限制符 (<see cref="MessageBase.Limiter"/>)。
        /// </summary>
        /// <returns>限制符的字节数组格式。</returns>
        private byte[] GetLimiter()
        {
            Limiter = FrameFormat == FrameFormats.Short ? (byte)0x2 : (byte)0x82;
            return new[] { Limiter };
        }

        /// <summary>
        /// 将设备地址设置为广播地址并将其转换为字节数组。
        /// </summary>
        private static byte[] SetAddress(bool isSecondaryMaster) =>
            isSecondaryMaster ? BitConverter.GetBytes(0x80000) : BitConverter.GetBytes(0x00000);

        /// <summary>
        /// 将设备地址转换为字节数组。
        /// </summary>
        /// <param name="isSecondaryMaster">如果该主设备是次主设备，则为 <see langword="true"/>。</param>
        /// <param name="deviceAddress">设备地址。</param>
        /// <returns>设备地址的字节数组格式。</returns>
        private static byte[] SetAddress(bool isSecondaryMaster, int deviceAddress)
        {
            if (deviceAddress < 0 || deviceAddress > 15)
                throw new ArgumentOutOfRangeException(nameof(deviceAddress), "短帧格式的设备地址必须在 0..15 范围内");

            var address = new BitArray(8);
            var bDeviceAddress = new BitArray(BitConverter.GetBytes(deviceAddress));

            address.Set(7, isSecondaryMaster);

            for (var i = 0; i < 4; i++)
                address.Set(i, bDeviceAddress[i]);

            var result = new byte[1];
            address.CopyTo(result, 0);

            return result;
        }

        /// <summary>
        /// 将设备地址转换为字节数组。
        /// <para>仅适用于长帧格式 (<see cref="FrameFormats.Long"/>)。</para>
        /// </summary>
        /// <param name="isSecondaryMaster">如果该主设备是次主设备，则为 <see langword="true"/>。</param>
        /// <param name="manufacturerId">制造商 ID。</param>
        /// <param name="deviceTypeCode">设备类型代码。</param>
        /// <param name="deviceSerialNumber">设备序列号。</param>
        /// <returns>设备地址的字节数组格式。</returns>
        private static byte[] SetAddress(bool isSecondaryMaster, int manufacturerId, int deviceTypeCode, int deviceSerialNumber)
        {
            if (manufacturerId < 0)
                throw new ArgumentException("制造商 ID 的值不能小于零。");

            if (deviceTypeCode < 0)
                throw new ArgumentException("设备类型代码的值不能小于零。");

            if (deviceSerialNumber < 0)
                throw new ArgumentException("设备序列号的值不能小于零。");

            var bManufacturerId = new BitArray(BitConverter.GetBytes(manufacturerId));
            var bDeviceTypeCode = new BitArray(BitConverter.GetBytes(deviceTypeCode));
            var bDeviceSerialNumber = new BitArray(BitConverter.GetBytes(deviceSerialNumber));
            var address = new BitArray(40);

            address.Set(39, isSecondaryMaster);

            for (var i = 0; i < 6; i++)
                address.Set(32 + i, bManufacturerId[i]);

            for (var i = 0; i < 8; i++)
                address.Set(24 + i, bDeviceTypeCode[i]);

            for (var i = 0; i < 24; i++)
                address.Set(i, bDeviceSerialNumber[i]);

            var result = new byte[5];
            address.CopyTo(result, 0);

            return result;
        }

        /// <summary>
        /// 填充消息的命令。
        /// </summary>
        /// <returns>命令的字节数组格式。</returns>
        private byte[] GetCommand()
        {
            if (Command < 0 || Command > 65534)
                throw new ArgumentOutOfRangeException(nameof(Command), "命令编号必须在 0..65534 范围内");

            byte[] result;
            var bCommand = BitConverter.GetBytes(Command);

            if (Command <= 255)
            {
                result = new byte[1];
                result[0] = bCommand[0];
            }
            else
            {
                result = new byte[2];
                result[0] = bCommand[0];
                result[1] = bCommand[1];
            }

            return result;
        }

        /// <summary>
        /// 字节计数器 - 形成响应代码和传输/接收数据的字节数。
        /// </summary>
        /// <param name="data">用于计数的数据。</param>
        /// <returns></returns>
        private static byte[] GetByteCounter(byte[] data)
        {
            var result = new byte[1];

            if (data != null)
                result = BitConverter.GetBytes(data.Length);

            return new[] { result[0] };
        }

        /// <summary>
        /// 获取校验和。
        /// </summary>
        /// <param name="data">用于计算的数据。</param>
        private byte[] GetCheckSum(byte[] data)
        {
            var result = new byte[1];

            for (var i = Preamble; i < data.Length - 1; i++)
                result[0] ^= data[i];

            return result;
        }
    }
}
